
import java.text.*;
import java.io.*;
import java.util.*;
import gnu.io.*;

/**
  * Logger.
  * This class is runnable and will connect to
  * serial ports as needed and do the logging if necessary.
  */

public class Logger implements Runnable, SerialPortEventListener
{
  /* Device types */
  final static int BGEIGIE = 0; // THS bGeigie device
  final static int BGEIGIE_MINI = 1; // Akiba's ninjaGeigie device
  final static String[] deviceList = {"bGeigie","bGeigie-mini"};
  final static int[] deviceCPM2uSphConvFactor = { 350, 350 };

  /* Device type. Will take one of the values above */
  int devType;

  /* drive info */
  String driverName;
  String tripInfo;

  /* All you need to write the log file */
  final static String BASENAME = "safecast";
  final static String EXTENSION = ".log";
  File logDir;
  String logFileName;
  PrintWriter logFile = null;

  /* bGeigie serial device */
  SerialLineReader bGeigie = null;
  int bGeigieRate = 9600;
  int bGeigieMiniRate = 9600;
  boolean bGeigieConnected = false;

  /* GPS serial device */
  SerialLineReader gps = null;
  int gpsRate = 38400;
  boolean gpsConnected;

  /* static variables that need to go at some point */
  String bGeigiePortName = null;
  String gpsPortName = null;

  /* Data to log */
  Data data;

  /* Thread attributes */
  Thread logThread;

  /* write interval */
  final static int DEFAULT_WRITE_INTERVAL = 5000;
  int writeInterval;
  int timeout;

  /* Some state variables */
  boolean loggingNow = false;

  /* Some print writers for normal and error output */
  PrintStream out;
  PrintStream err;

  /**
    * Constructors.
    */
  Logger()
  {
    init(DEFAULT_WRITE_INTERVAL, new File("."), System.out, System.err);
  }
  // name of log directory as arg
  Logger(int writeInterval, String logDir)
  {
    init(writeInterval, new File(logDir), System.out, System.err);
  }
  // name of log directory as arg
  Logger(int writeInterval, String logDir, PrintStream out, PrintStream err)
  {
    init(writeInterval, new File(logDir), out, err);
  }
  // name of log directory as arg
  Logger(String logDir)
  {
    init(DEFAULT_WRITE_INTERVAL, new File(logDir), System.out, System.err);
  }
  // same with argument
  Logger (int writeInterval)
  {
    init(writeInterval, new File("."), System.out, System.err);
  }
  // same with argument
  Logger (File logDir)
  {
    init(DEFAULT_WRITE_INTERVAL, logDir, System.out, System.err);
  }
  // same with argument
  Logger (int writeInterval, File logDir)
  {
    init(writeInterval, logDir, System.out, System.err);
  }
  // same with argument
  Logger (int writeInterval, File logDir, PrintStream out, PrintStream err)
  {
    init(writeInterval, logDir, out, err);
  }

  /* init function */
  void init(int writeInterval, File logDir, PrintStream out, PrintStream err)
  {
    // set the interval at which we write the data to the log
    this.writeInterval = writeInterval;

    // set timeout to 4x this
    timeout = 4*this.writeInterval;

    // output streams
    this.out = out;
    this.err = err;

    // new data object
    data = new Data();

    // check logDir exists and stuff
    if (logDir.isDirectory() && logDir.exists() && logDir.canWrite())
      this.logDir = logDir;
    else  // just use current directory
      this.logDir = new File(".");
  }

  double CPM2uSph(int cpm)
  {
    if (devType >= 0)
      return (double)data.getCPM()/(double)deviceCPM2uSphConvFactor[devType];
    else
      return 0.0;
  }

  double counts2uS(int counts)
  {
    // X CPM = 60X CPH = AX uSph
    // <=> X CPH = A/60 X uSph
    // <=> X Counts = A/60 X uS
    if (devType >= 0)
      return (double)data.getDose()/((double)deviceCPM2uSphConvFactor[devType]*60.0);
    else
      return 0.0;
  }

  void printLogFileHeader()
  {
    logFile.println("# DriverName = " + driverName);
    logFile.println("# TripInfo = " + tripInfo);
    if (devType != -1)
      logFile.println("# DeviceType = " + deviceList[devType]);
    else
      logFile.println("# DeviceType = !!Missing data");
    logFile.println("# Format =");
    logFile.println("# 0 - Device Tag");
    logFile.println("# 1 - Device Serial ID");
    logFile.println("# 2 - Date and time UTC, ISO8601, yyyy-MM-ddThh:mm:ssZ");
    logFile.println("# 3 - CPM");
    logFile.println("# 4 - Counts Per 5 Seconds");
    logFile.println("# 5 - TotalCounts");
    logFile.println("# 6 - CPM Validity Flag, A=available, V=void");
    logFile.println("# 7 - Latitude (NMEA format)");
    logFile.println("# 8 - North/South indicator");
    logFile.println("# 9 - Longitude (NMEA format)");
    logFile.println("# 10 - East/West indicator");
    logFile.println("# 11 - Altitude");
    logFile.println("# 12 - GPS Fix indicator A=available, V=void");
    logFile.println("# 13 - Horizontal Dillution of Precision (GPS)");
    logFile.println("# 14 - GPS Fix quality indicator (0=bad, 1=good, 2,3,...=not so good)");
  }

  /* Starts the logging yeah! */
  public void start()
  {
    if ((devType == BGEIGIE && bGeigieConnected && gpsConnected) ||
                            (devType == BGEIGIE_MINI && bGeigieConnected))
    {
      // try to open a new log file
      try
      {
        updateFileName();
        logFile = openFile(logFileName);
        /* Add info in comment at beggining of file */
        printLogFileHeader();
      }
      catch (FileNotFoundException fnfe)
      {
        err.println("Log file not found.");
        return;
      }
      catch (IOException ioe)
      {
        err.println("IO Exception.");
        return;
      }

      /* create and start thread */
      logThread = new Thread(this);
      // first need to open the logfile
      out.println("Start logging to file " + logFileName);
      data.reset();      // reset data, associated timeouts, etc.
      loggingNow = true; // from now on we are logging shit
      logThread.start(); // Start thread
    } else {
      out.println("Please connect a device first.");
    }
  }

  /* Stop logging bouh */
  public void stop()
  {
    if (loggingNow)
    {
      // just set loggingNow to false and the thread should stop */
      loggingNow = false;
      // flush whatever is left
      logFile.flush();
      // close log file
      closeFile(logFile);
      // print nice message
      out.println("Stop logging - Log file is " + logFileName);
      // interrupt thread to stop it
      logThread.interrupt();
    }
  }

  /* returns true if we are currently logging */
  public boolean isLogging()
  {
    return loggingNow;
  }

  /* This one is for the good ol' bGeigie with both arduino and GPS */
  public boolean connect(String bGeigiePortName, String gpsPortName)
  {
    /* connect to bGeigie */
    bGeigie = connectSerialDev(bGeigiePortName, bGeigieRate);
    if (bGeigie == null)
      return false;
    bGeigieConnected = true;

    /* connect to GPS */
    gps = connectSerialDev(gpsPortName, gpsRate);
    if (gps == null)
    {
      disconnect(); // to disconnect bGeigie
      return false;
    }
    gpsConnected = true;

    /* seems we could connect to both device */
    devType = BGEIGIE;  // we are using a good ol' bGeigie
    out.println("bGeigie connected.");
    return true;

  }

  /* This one is for the good Akiba's bGeigie mini */
  public boolean connect(String bGeigiePortName)
  {
    /* connect to bGeigie */
    bGeigie = connectSerialDev(bGeigiePortName, bGeigieRate);
    if (bGeigie == null)
      return false;
    bGeigieConnected = true;

    /* seems we could connect to both device */
    devType = BGEIGIE_MINI;  // we are using a good ol' bGeigie
    out.println("bGeigie-mini connected.");
    return true;

  }

  /* returns true if we are connected */
  public boolean isConnected()
  {
    boolean caseBGeigie = (devType == BGEIGIE && bGeigieConnected && gpsConnected);
    boolean caseBGeigieMini = (devType == BGEIGIE_MINI && bGeigieConnected);
    return (caseBGeigie || caseBGeigieMini);
  }

  /* connect to a serial port and create a Serial Line Reader object */
  private SerialLineReader connectSerialDev(String devPortName, int rate)
  {
    /* need that to find the serial ports */
    CommPortIdentifier portId = null;

    /* a new SerialLineReader */
    SerialLineReader serialReader = null;

    /* some sweet variable to track presence of serial ports */
    boolean         devPortFound = false;

    /* try to find the ports */
    try
    {
      portId = CommPortIdentifier.getPortIdentifier(devPortName);
      out.println("Found port " + devPortName);
      devPortFound = true;
    }
    catch (NoSuchPortException nspe)
    {
      err.println("No such port: " + devPortName);
    }

    if (devPortFound)
    {
      try {
        serialReader = new SerialLineReader(portId, rate, this);
      }
      catch (PortInUseException e) {
        err.println("Port " + devPortName + " already in use - aborting");
      }
      catch (IOException e) {
        err.println("IOException - can't get " + devPortName + " stream");
      }
      catch (TooManyListenersException e) {
        err.println("Too many listeners on port " + devPortName + " - aborting");
      }
      catch (UnsupportedCommOperationException e) {
        err.println("Unsupported Comm operation on port " + devPortName
            + " - aborting");
      }
    }

    return serialReader;
  }

  /* disconnect serial ports */
  public void disconnect()
  {
    /* try to disconnect bGeigie */
    if (bGeigieConnected)
    {
      bGeigie.close();
      bGeigieConnected = false;
    }

    /* if we need to disconnect GPS too */
    if (devType == BGEIGIE && gpsConnected)
    {
      gps.close();
      gpsConnected = false;
    }

    /* reset device type */
    devType = -1;
  }

  void setLogDir(File dir)
  {
    if (dir.exists() && dir.isDirectory() && dir.canWrite())
      logDir = dir;
  }

  /* open a file in the log directory */
  PrintWriter openFile(String filename) throws FileNotFoundException, IOException
  {
    PrintWriter newfile;
    // create file
    File file = new File(logDir, filename);
    file.createNewFile();
    // create new print writer
    newfile = new PrintWriter(file);

    return newfile;
  }

  void closeFile(PrintWriter pw)
  {
    pw.close();
  }

  void updateFileName()
  {
    Date now = new Date();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");
    logFileName = BASENAME + "-" + sdf.format(now.getTime()) + EXTENSION;
  }

  /* data getter */
  Data getData() { return data; }

  /* driver setter */
  void setDriverName(String name)
  {
    driverName = name;
  }

  /* trip info setter */
  void setTripInfo(String info)
  {
    tripInfo = info;
  }

  /**
    * Run method
    * @see
    */
  public void run()
  {
logging:  // main logging loop
      while (loggingNow)  // while loggingNow is true
      {
        try 
        {
          Thread.sleep(timeout);
          // if we reach this here we timeout!
          err.println("Timeout waiting for new data... Stop and disconnect.");
          stop();
          disconnect();
        }
        catch (InterruptedException ie)
        {
          if (loggingNow)
            out.println(data.getLogEntry() + " -> Logged.");
        }
      }
  }

  /**
    * serialEvent method
    * @see
    */
  public void serialEvent(SerialPortEvent spe)
  {
    switch (spe.getEventType()) {
      case SerialPortEvent.BI:
      case SerialPortEvent.OE:
      case SerialPortEvent.FE:
      case SerialPortEvent.PE:
      case SerialPortEvent.CD:
      case SerialPortEvent.CTS:
      case SerialPortEvent.DSR:
      case SerialPortEvent.RI:
      case SerialPortEvent.OUTPUT_BUFFER_EMPTY:
        break;
      case SerialPortEvent.DATA_AVAILABLE:

        /* Read from GPS device */
        if (devType == BGEIGIE && gps.triggered(spe))
        {
          try
          {
            while (gps.lineAvailable())
            {
              String sentence = gps.readLine();
              data.parseData(sentence);
            }
          }
          catch (IOException ioe)
          {
            err.println("Serial Error: can't read data anymore. Stopping.");
            stop();
            disconnect();
          }
        }

        /* Read from bGeigie device */
        if (bGeigie.triggered(spe))
        {
          try
          {
            while(bGeigie.lineAvailable())
            {
              String sentence = bGeigie.readLine();
              data.parseData(sentence);
            }
          }
          catch (IOException ioe)
          {
            err.println("Serial Error: can't read data anymore. Stopping.");
            stop();
            disconnect();
          }
        }

        // if data is ready, and we are logging, print to log
        if (data.ready())
        {
          String logEntry = data.getLogEntry();
          if (loggingNow)
          {
            // print to log file
            logFile.println(logEntry);
            logFile.flush();
            // request next data
            data.requestNext();
            // interrupt thread to signal new data logged
            logThread.interrupt();
          }
        }

        break;
    }

  }
}
